#!/usr/local/bin/perl
# leeym@leeym.com
# $Id$
use FindBin qw($Bin $Script);
use Getopt::Long;
use File::Find;
use File::Path;
use File::Copy;
use File::Basename;
use Data::Dumper;
use Config;
use Config::IniFiles;
use Sys::Hostname;
use strict;

my %opt;

GetOptions \%opt, qw(
  --verbose
  --debug
  --quiet
  --list
  --install
  --deinstall
  --dump
  --create
  --remove
  --help
  --user
  --mount
  --umount
  --set
  --unset
  --all
);

@ARGV = () if $opt{all};

my %LIVECD;
$LIVECD{'HOME'} = "/var/$Script";
foreach my $d ('ROOT', 'IMAGE')
{
  $LIVECD{$d} = $LIVECD{'HOME'} . "/" . $d;
  mkpath($LIVECD{$d}, $opt{verbose});
}

my $whoami = (getpwuid($<))[0];

die "$Script: You are not root!\n" if $<;

my $ini_file = $LIVECD{'HOME'} . "/$Script.ini";
if (!-f $ini_file)
{
  open(F, ">$ini_file") || die;
  print F "[HOME]\n";
  print F "createtime=" . time() . "\n";
  close(F);
}
tie(my %ini, 'Config::IniFiles', (-file => $ini_file)) || die @Config::IniFiles::errors;

_set('HOME', undef, "TRYBROKEN=yes");
_set('HOME', undef, "FORCE_PACKAGE=yes");
_set('HOME', undef, "DEPENDS_TARGET=package");
_set('HOME', undef, "USE_PACKAGE_DEPENDS=yes");
_set('HOME', undef, "WRKDIRPREFIX=/usr/obj");
_set('HOME', undef, "X11BASE=/usr/local");

exit list(@ARGV)      if $opt{list};
exit install(@ARGV)   if $opt{install};
exit deinstall(@ARGV) if $opt{deinstall};
exit &dump(@ARGV)     if $opt{dump};
exit create(@ARGV)    if $opt{create};
exit remove(@ARGV)    if $opt{remove};
exit mount(@ARGV)     if $opt{mount};
exit umount(@ARGV)    if $opt{umount};
exit set(@ARGV)       if $opt{set};
exit unset(@ARGV)     if $opt{unset};
help()                if $opt{help};

my $root = shift;
exit execute($root, @ARGV) if $root;

help();

# END

END
{
  map { delete($ini{$_}) if !scalar(%{ $ini{$_} }) } keys %ini;
  tied(%ini)->WriteConfig($ini_file) if %ini;
}

sub execute
{
  my $ret = _execute(1, @_);
  return $ret ? $ret>>8 : $ret;
}

sub _execute
{
  my $verbose = shift;
  my $_root   = shift;
  my $cmd     = join(' ', @_);
  $_root =~ s{/$}{};
  $_root =~ s{.*/}{};
  help() if !$_root;
  my $shell   = $ENV{'SHELL'};
  $shell = '/bin/tcsh' if ! -f "$root/bin/$shell";
  my $realcmd = $cmd ? "$shell -c '$cmd'" : $shell;
  my $root    = $LIVECD{'ROOT'} . "/$_root";
  die "$Script: ROOT:$_root not found" if !-d $root;
  mount($_root);
  my $env = $ini{"ROOT:$_root"}{'ENV'};
  foreach my $envk (sort keys %ENV)
  {
    $env .= sprintf(" %s=\"%s\"", $envk, $ENV{$envk});
  }
  print "\nEntering: isoroot $_root $cmd\n\n" if $verbose;
  my $ret = System("env $env /usr/sbin/chroot $root $realcmd");
  printf("\nReturning(%d): isoroot $_root $cmd\n\n", $ret) if $verbose;
  return $ret;
}

sub System
{
  my $cmd = shift;
  print "$cmd\n" if $opt{verbose};
  return system($cmd);
}

sub create
{
  return map { create($_, $_) if !$ini{'ROOT'}{$_}; } keys %{ $ini{'IMAGE'} } if !scalar(@_) and $opt{all};
  my $_root  = shift;
  my $_image = shift;
  help() if !$_root;
  map { die "$Script: Invalid ROOT:$_root" if $_root =~ /$_/ } keys %LIVECD;
  chomp($_image = uc $Config{'osvers'}) if !$_image;
  my $image = $LIVECD{'IMAGE'} . "/" . basename($_image);
  die "$Script: IMAGE:$image not found" if !-d $image;
  my $root = $LIVECD{'ROOT'} . "/$_root";
  die "$Script: ROOT:$_root exists already"                  if -d $root;
  print "$Script: Creating ROOT:$_root from IMAGE:$_image\n" if !$opt{quiet};
  find(
    sub {
      (my $src = $File::Find::name) =~ s|^$image||;
      (my $dst = $File::Find::name) =~ s|^$image|$root|;
      if (-l $File::Find::name)
      {
        my $ln = readlink($File::Find::name);
        print "l $src -> $ln\n" if $opt{debug};
        symlink($ln, $dst);
      }
      elsif (-d $File::Find::name)
      {
        print "d $src\n" if $opt{debug};
        mkpath($dst);
      }
      elsif (-f $File::Find::name)
      {
        print "f $src\n" if $opt{debug};
        link($File::Find::name, $dst);
      }
      else
      {
        print "? $src\n" if $opt{debug};

        #link($File::Find::name, $dst);
      }
    },
    $image
  );

  chmod(0777, "$root/tmp", "$root/var/tmp", "$root/var/tmp/vi.recover");

  my $packages = "/usr/ports/packages-" . substr($_image, 0, 1);
  for my $d ('/home', '/usr/ports', $packages)
  {
    mkpath("$root$d", $opt{verbose});
  }

  copy("$root/usr/share/zoneinfo/Asia/Taipei", "$root/etc/localtime");

  for my $f (glob("/etc/*passwd /etc/group /etc/resolv.conf /etc/ssh/*"))
  {
    copy($f, "$root/$f") || die "$Script: Cant copy $f: $!";
    my $mode = (stat($f))[2];
    chmod($mode & 07777, "$root/$f");
  }
  _execute(0, $root, "/usr/sbin/pwd_mkdb /etc/master.passwd");
  _execute(0, $root, "/sbin/ldconfig -elf /usr/lib /usr/lib/compat");
  _execute(0, $root, "/sbin/ldconfig -aout /usr/lib/aout /usr/lib/compat/aout");

  $ini{'ROOT'}{$_root}         = $root;
  $ini{"ROOT:$_root"}{'ctime'} = time();
  $ini{"ROOT:$_root"}{'image'} = $_image;

  # FS
  my $memfs = 'mfs:md';
  $memfs = 'tmpfs:tmpfs' if -e '/boot/kernel/tmpfs.ko';
  my @FS;
  push(@FS, 'devfs:/dev');
  push(@FS, 'procfs:/proc');
  push(@FS, 'nullfs:/usr/ports');
  push(@FS, 'nullfs:/home');
  push(@FS, "$memfs:/tmp");
  push(@FS, "$memfs:/usr/obj");
  push(@FS, "$memfs:/var/tmp");
  push(@FS, "$memfs:/var/db/pkg");
  push(@FS, "$memfs:/usr/local");
  $ini{"ROOT:$_root"}{'fstab'} = join(' ', @FS);

  # ENV
  set($_root, "ISOROOT_NAME=$_root");
  set($_root, "PACKAGES=$packages");
}

sub set
{
  _set('ROOT', @_);
}

sub _set
{
  my $prefix = shift;
  my $name   = shift;
  my @pairs  = @_;
  my $_name  = ($prefix && $name) ? "$prefix:$name" : "$prefix$name";
  my %_ENV   = getHash($ini{"$_name"}{'ENV'});
  my $_image = $ini{"$prefix:$name"}{'image'};
  foreach my $pair (split(' ', $ini{'HOME'}{'ENV'}),
    @pairs, split(' ', $ini{"IMAGE:$_image"}{'ENV'}))
  {
    my ($k, $v) = ($1, $2) if $pair =~ /^([^=]+)=(.+)$/;
    die "$Script: $pair isn't key=value" if !length($k) || !length($v);
    $_ENV{$k} = $v;
  }
  $ini{"$_name"}{'ENV'} = setHash(%_ENV);
}

sub unset
{
  _unset('ROOT', @_);
}

sub _unset
{
  my $prefix = shift;
  my $name   = shift;
  die "$Script: $prefix:$name not found" if !exists($ini{"$prefix:$name"});
  my @K = @_;
  return if !scalar(@K);
  my %_ENV = getHash($ini{"$prefix:$name"}{'ENV'});
  map { delete($_ENV{$_}) } @K;
  my $_image = $ini{"$prefix:$name"}{'image'};

  foreach my $pair (split(' ', $ini{'HOME'}{'ENV'}))
  {
    my ($k, $v) = ($1, $2) if $pair =~ /^([^=]+)=(.+)$/;
    die "$Script: $pair isn't key=value" if !length($k) || !length($v);
    $_ENV{$k} = $v;
  }
  $ini{"$prefix:$name"}{'ENV'} = setHash(%_ENV);
}

sub fstab
{
  my $_root  = shift;
  my $action = shift;
  die "$Script: unknown action: $action" if $action !~ /^u?mount$/;
  my @FS = split(' ', $ini{"ROOT:$_root"}{'fstab'});
  my $root = "$LIVECD{'ROOT'}/$_root";
  die "$Script: ROOT:$_root not found" if !-d $root;
  my %MOUNT;
  open(M, "/sbin/mount |") || die $!;

  while (<M>)
  {
    my ($src, $dst, $type) = ($1, $2, $3) if m{^(\S+) on (\S+) \((\w+),?.*?\)};
    $MOUNT{$dst} = "$type:$src:$dst";
  }
  close(M);
  foreach my $fs (@FS)
  {
    my ($type, $src, $dst) = split(':', $fs);
    $dst = $src if !$dst;
    mkdir("$root$dst") if ! -d "$root$dst";
    my $cmd;
    if ($action eq 'mount')
    {
      next if exists($MOUNT{"$root$dst"});
      $cmd = "$action -t ${type} $src $root$dst";
      $cmd = "mdmfs -s 2G $src $root$dst" if $type eq 'mfs';
    }
    elsif ($action eq 'umount')
    {
      next if !exists($MOUNT{"$root$dst"});
      my $_src = (split(':', $MOUNT{"$root$dst"}))[1];
      $cmd = "$action -f $root$dst";
      $cmd .= " && mdconfig -d -u $_src" if $type eq 'mfs';
    }
    System($cmd);
  }
}

sub mount
{
  return map { mount($_) } keys %{ $ini{'ROOT'} } if !scalar(@_) && $opt{all};
  fstab(shift, 'mount');
}

sub umount
{
  return map { umount($_) } keys %{ $ini{'ROOT'} } if !scalar(@_) && $opt{all};
  fstab(shift, 'umount');
}

sub install
{
  foreach my $image (@_)
  {
    help() if !-f $image;
    my $_image = image2uname($image);
    die "$Script: not a valid ISO file: $image" if !$_image;
    my $T = "$LIVECD{'IMAGE'}/$_image";
    mkpath($T, $opt{verbose});
    print "$Script: Installing IMAGE:$image\n" if !$opt{quiet};
    my $t = "/tmp/$Script.$$";
    mkpath($t, $opt{verbose});
    my $vnode = vnode();

    if (-x '/sbin/mdconfig')
    {
      $vnode = 'md' . $vnode;
      System("/sbin/mdconfig -a -t vnode -f $image -u $vnode");
    }
    else
    {
      $vnode = 'vn' . $vnode . 'c';
      System("vnconfig -c $vnode $image");
    }
    System("/sbin/mount_cd9660 /dev/$vnode $t");
    $ini{'IMAGE'}{$_image} = $T;

    # livefs
    my $mtree = mtree("$t/etc/mtree/BSD.root.dist");
    if ($mtree)
    {
      my $tar = '/usr/bin/tar';
      my $taropt = $opt{debug} ? "-xpvf" : "-xpf";
      System("$tar -C $t -cf - $mtree | $tar -C $T $taropt -");
    }

    # miniinst
    foreach my $prefix ("$t", "$t/$_image")
    {
      foreach my $dir ("base", "bin", "kernels", "src")
      {
        next if !-f "$prefix/$dir/install.sh";
        mkpath("$T/usr/src", $opt{verbose});
        my $cmd = "cd $prefix/$dir && echo y | env DESTDIR=$T sh ./install.sh";
        $cmd .= " generic" if $dir eq 'kernels';
        $cmd .= " all" if $dir eq 'src';
        $cmd .= " > /dev/null" if !$opt{debug};
        System($cmd);
      }
    }
    System("/bin/chflags -R noschg $T");

    my $osversion = OSVERSION($T);
    my $osrel     = OSREL($T);
    _set('IMAGE', $_image, "OSVERSION=$osversion") if $osversion;
    _set('IMAGE', $_image, "OSREL=$osrel")         if $osrel;

    move("$T/usr/bin/uname", "$T/usr/bin/.uname");
    open(F, ">$T/usr/bin/uname") || die $!;
    print F uname($T);
    close(F);
    chmod(0755, "$T/usr/bin/uname");

    if (-x '/sbin/mdconfig')
    {
      System("/sbin/umount $t && /sbin/mdconfig -d -u $vnode && rmdir $t");
    }
    else
    {
      System("/sbin/umount $t && /sbin/vnconfig -u $vnode && rmdir $t");
    }
  }
}

sub GREP
{
  my $file    = shift;
  my $pattern = shift;
  open(F, $file) || return;
  my $str;
  while (<F>)
  {
    $str = $1 if /$pattern/;
    last if $str;
  }
  close(F);
  die "$Script: $pattern not found in $file" if !$str;
  return $str;
}

sub OSVERSION
{
  my $root = shift;
  GREP("$root/usr/include/sys/param.h", '^#define __FreeBSD_version (\d+)');
}

sub mtree
{
  my $file = shift;
  open(F, $file) || return;
  my @DIRS;
  while (<F>)
  {
    chomp(my $line = $_);
    next if $line =~ /^#/;
    $line =~ s/^\s{4}//g;
    next if $line !~ /^\w/;
    $line =~ s/\s.*//g;
    push(@DIRS, $line);
  }
  close(F);
  return join(' ', @DIRS);
}

sub deinstall
{
  return map { deinstall($_) } keys %{ $ini{'IMAGE'} } if ($opt{all} && !scalar(@_));
  help() if !scalar(@_);
  foreach my $_image (@_)
  {
    my $image = $LIVECD{'IMAGE'} . "/$_image";
    next                                          if !-d $image;
    print "$Script: Deinstalling IMAGE:$_image\n" if !$opt{quiet};
    System("/bin/rm -fr $image");
    $ini{"IMAGE:$_image"} = {};
    delete($ini{"IMAGE:$_image"});
    delete($ini{'IMAGE'}{$_image});
  }
}

sub remove
{
  return map { remove($_) } keys %{ $ini{'ROOT'} } if !scalar(@_) && $opt{all};
  my $_root = shift;
  help() if !$_root;
  my $root = $LIVECD{'ROOT'} . "/$_root";
  die "$Script: ROOT:$root not found" if !-d $root;
  umount($_root);
  print "$Script: Removing ROOT:$_root\n" if !$opt{quiet};
  System("/bin/rm -fr $root");
  $ini{"ROOT:$_root"} = {};
  delete($ini{"ROOT:$_root"});
  delete($ini{'ROOT'}{$_root});
}

sub help
{
  die <<END;
Usage:
	$Script [options(s)] [command]

Options:
	--verbose
	--debug
	--user		<user id>

Commands:
	--help
	--list
	--install	<image file1> [<image file2> ...]
	--deinstall	<image name1> [<image name2> ...]
	--deinstall --all
	--create	<root name> <installed image name>
	--create --all
	--remove	<root name>
	--remove --all
	--set		<root name> <key1>=<value1> [<key2>=<value2> ...]
	--unset		<root name> <key1> [<key2> ...]
	<root name> <command> [<arguments> ...]

END
}

sub list
{
  my $sec = shift;
  map { delete($ini{$_}) if !scalar(keys %{ $ini{$_} }); } (keys %ini);
  foreach my $type ('IMAGE', 'ROOT')
  {
    opendir(D, $LIVECD{$type}) || die $!;
    foreach my $l (grep { !/^\./ && -d $LIVECD{$type} . "/" . $_; } readdir(D))
    {
      $ini{$type}{$l} = $LIVECD{$type} . "/$l";
    }
    closedir(D);
  }
  if ($ini{$sec})
  {
    print Dumper($ini{$sec});
  }
  else
  {
    print join("\n", sort keys %ini), "\n";
  }
  return 0;
}

sub image2uname
{
  my $image = shift;
  $image =~ s{-\w+-(disc\d+|livefs)\.iso}{}ig;
  $image =~ s{.*/}{}ig;
  my ($uname, $arch) = ($1, $2) if $image =~ /(.+)-?([a-z\d]+)?/;
  my $_arch = $Config{'archname'};
  $_arch = $1 if $_arch =~ /^([^-]+)-/;
  return if ($arch && $arch ne $_arch);
  return $uname;
}

sub getHash
{
  splitHash('=', ' ', @_);
}

sub setHash
{
  joinHash('=', ' ', @_);
}

sub splitHash
{
  my $sep1 = shift;
  my $sep2 = shift;
  my $str  = shift;
  my %hash;
  foreach my $pair (split($sep2, $str))
  {
    my ($k, $v) = split($sep1, $pair);
    next if !length($v);
    $hash{$k} = $v;
  }
  return %hash;
}

sub joinHash
{
  my $sep1 = shift;
  my $sep2 = shift;
  my %hash = @_;
  my $str;
  foreach my $k (sort keys %hash)
  {
    $str .= $sep2 if $str;
    $str .= "$k$sep1$hash{$k}";
  }
  return $str;
}

sub dump
{
  print Dumper(\%ini);
  return 0;
}

sub vnode
{
  opendir(DIR, '/dev') || die "can't opendir /dev: $!";
  my %vnode;
  map { $vnode{$_} = $_; } grep { /^(md|vn)\d+/ } readdir(DIR);
  closedir DIR;
  for (my $i = 0; $i < 1024; $i++)
  {
    return $i if (!exists($vnode{"md$i"}) && !exists($vnode{"vn$i"}));
  }
  die "No vnode available";
}

sub kernel
{
  my $root = shift;
  foreach my $kernel (qw(/boot/kernel/kernel /boot/GENERIC/kernel))
  {
    return "$root$kernel" if -f "$root$kernel";
  }
  die "kernel not found";
}

sub build_time
{
  my $root    = shift;
  my $kernel  = kernel($root);
  my $strings = `strings $kernel`;
  return $1 if $strings =~ m{#\d+: (\w+ \w+\s+\d+ \d\d:\d\d:\d\d \w+ \d+)};
}

sub build_dir
{
  my $root    = shift;
  my $kernel  = kernel($root);
  my $strings = `strings $kernel`;
  return $1 if $strings =~ m{(\S+\@\S+:/\S+)};
}

sub identity
{
  my $root    = shift;
  my $kernel  = kernel($root);
  my $strings = `strings $kernel`;
  return $1 if $strings =~ m{\S+\@\S+:/\S+/(.+)};
}

sub machine
{
  my $root    = shift;
  my $kernel  = kernel($root);
  my $strings = `strings $kernel`;
  return $1 if $strings =~ m{(\w+)_extend_pcb}m;
}

sub OSREL
{
  my $root    = shift;
  my $kernel  = kernel($root);
  my $strings = `strings $kernel`;
  my $osrel   = $1 if $strings =~ m{(\d+(?:\.\d+)+)-[A-Z]+};
  return $osrel;
}

sub release
{
  my $root    = shift;
  my $kernel  = kernel($root);
  my $strings = `strings $kernel`;
  return $1 if $strings =~ m{(\d+(?:\.\d+)+-[A-Z]+)};
}

sub os
{
  my $root    = shift;
  my $kernel  = kernel($root);
  my $strings = `strings $kernel`;
  return $1 if $strings =~ m{(\w+) \d+(?:\.\d+)+-[A-Z]+}m;
}

sub uname
{
  my $root       = shift;
  my $identity   = identity($root) || die $!;
  my $machine    = machine($root) || die $!;
  my $release    = release($root) || die $!;
  my $os         = os($root) || die $!;
  my $build_time = build_time($root) || die $!;
  my $build_dir  = build_dir($root) || die $!;
  my $hostname   = hostname() || die $!;
  return <<END;
#!/bin/sh
args=`getopt aimnprsv \$*`
if [ \$? -ne 0 ]; then
  echo usage: uname [-aimnprsv]
  exit 2
fi
set -- \$args
out=""
for x
do
  case "\$x"
  in
    -a)
      out="$os $hostname $release $os $release #0: $build_time     $build_dir  $machine"
      break;
      shift;;
    -s)
      if [ ! -z "\$out" ]; then out="\${out} "; fi; out="\${out}$os"
      shift;;
    -n)
      if [ ! -z "\$out" ]; then out="\${out} "; fi; out="\${out}$hostname"
      shift;;
    -r)
      if [ ! -z "\$out" ]; then out="\${out} "; fi; out="\${out}$release"
      shift;;
    -v)
      if [ ! -z "\$out" ]; then out="\${out} "; fi; out="\${out}$os $release #0: $build_time     $build_dir"
      shift;;
    -m)
      if [ ! -z "\$out" ]; then out="\${out} "; fi; out="\${out}$machine"
      shift;;
    -p)
      if [ ! -z "\$out" ]; then out="\${out} "; fi; out="\${out}$machine"
      shift;;
    -i)
      if [ ! -z "\$out" ]; then out="\${out} "; fi; out="\${out}$identity"
      shift;;
  esac
done
if [ -z "\$out" ]; then out="$os"; fi
echo "\$out"
END
}
